/**
 * utils是一个非特定的axios通用帮助函数库
 */
"use strict";

const bind = require("./helpers/bind");

const toString = Object.prototype.toString;

/**
 * 判断一个值是不是数组类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是数组，否则为false
 */
function isArray(val) {
  return toString.call(val) === "[object Array]";
}

/**
 * 判断一个值是不是undefined类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是undefined类型，否则为false
 */
function isUndefined(val) {
  return typeof val === "undefined";
}

/**
 * 判断一个值是不是Buffer类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是Buffer类型，否则为false
 */
function isBuffer(val) {
  return (
    val !== null &&
    !isUndefined(val) &&
    val.constructor !== null &&
    !isUndefined(val.constructor) &&
    typeof val.constructor.isBuffer === "function" &&
    val.constructor.isBuffer(val)
  );
}

/**
 * 判断一个值是不是ArrayBuffer类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是ArrayBuffer类型，否则为false
 */
function isArrayBuffer(val) {
  return toString.call(val) === "[object ArrayBuffer]";
}

/**
 * 判断一个值是不是FormData类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是FormData类型，否则为false
 */
function isFormData(val) {
  return typeof val !== "undefined" && val instanceof FormData;
}

/**
 * 判断一个值是不是ArrayBuffer上的视图
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是ArrayBuffer上的视图，否则为false
 */
function isArrayBufferView(val) {
  let result;
  if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView) {
    result = ArrayBuffer.isView(val);
  } else {
    result = val && val.buffer && val.buffer instanceof ArrayBuffer;
  }
  return result;
}

/**
 * 判断一个值是不是string类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是string类型，否则为false
 */
function isString(val) {
  return typeof val === "string";
}

/**
 * 判断一个值是不是number类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是number类型，否则为false
 */
function isNumber(val) {
  return typeof val === "number";
}

/**
 * 判断一个值是不是非null对象
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是Object类型，否则为false
 */
function isObject(val) {
  return val !== null && typeof val === "object";
}

/**
 * 判断一个值是不是Date类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是Date类型，否则为false
 */
function isDate(val) {
  return toString.call(val) === "[object Date]";
}

/**
 * 判断一个值是不是File类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是File类型，否则为false
 */
function isFile(val) {
  return toString.call(val) === "[object File]";
}

/**
 * 判断一个值是不是Blob类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是Blob类型，否则为false
 */
function isBlob(val) {
  return toString.call(val) === "[object Blob]";
}

/**
 * 判断一个值是不是Function类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是Function类型，否则为false
 */
function isFunction(val) {
  return toString.call(val) === "[object Function]";
}

/**
 * 判断一个值是不是Stream类型
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是Stream类型，否则为false
 */
function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}

/**
 * 判断一个值是不是URLSearchParams对象
 * @param {Object} val 需要判定的值
 * @returns {boolean} true表示是URLSearchParams对象，否则为false
 */
function isURLSearchParams(val) {
  return (
    typeof URLSearchParams !== "undefined" && val instanceof URLSearchParams
  );
}

/**
 * 去除字符串首尾的空格
 * @param {String} str 需要去除空格的字符串
 * @returns {String} 去除了首尾空格的字符串
 */
function trim(str) {
  return str.replace(/^\s*/, "").replace(/\s*$/, "");
}

/**
 * 判断是不是运行在标准的浏览器环境
 *
 * axios允许运行在web worker、react-native等环境。
 * 所有支持XMLHttpRequest的环境，但不完全支持标准的全局变量。
 *
 * web workers:
 *   typeof window -> undefined
 *   typeof document -> undefined
 *
 * react-native:
 *   navigator.product -> 'ReactNative'
 * nativescript
 *   navigator.product -> 'NativeScript' or 'NS'
 */
function isStandardBrowserEnv() {
  if (
    typeof navigator !== "undefined" &&
    (navigator.product === "ReactNative" ||
      navigator.product === "NativeScript" ||
      navigator.product === "NS")
  ) {
    return false;
  }
  return typeof window !== "undefined" && typeof document !== "undefined";
}

/**
 * 遍历一个数组或一个对象，为每个项目调用一个函数。
 *
 * 如果`obj`是一个数组，则回调函数参数为传递每个项目的值，索引和完整数组。
 *
 * 如果`obj`是一个对象，则回调函数参数为传递每个项目的值，属性名和完整对象。
 *
 * @param {Object|Array} obj 需要遍历的对象或数组
 * @param {Function} fn 处理数组或对象的每一项时使用的回调函数
 */
function forEach(obj, fn) {
  // obj没有值则退出
  if (obj === null || isUndefined(obj)) {
    return;
  }

  // 如果不是可以遍历的东西就强制转换为数组
  if (typeof obj !== "object") {
    obj = [obj];
  }

  if (isArray(obj)) {
    // 遍历数组的值
    for (let i = 0; i < obj.length; i++) {
      fn.call(obj[i], i, obj);
    }
  } else {
    // 遍历对象的key
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        const element = obj[key];
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

/**
 * 接收多个参数（预期参数是对象），然后拷贝参数属性合并到一个对象中返回
 *
 * 当多个对象包含相同的键时，后面对象的参数优先
 *
 * 示例：
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // 输出 456
 * ```
 *
 * @param  {...any} args 需要合并的对象
 * @returns {Object} 所有合并属性的结果
 */
function merge(...args) {
  const result = {};
  function assignValue(val, key) {
    if (isObject(result[key]) && isObject(val)) {
      result[key] = merge(result[key], val);
    } else {
      result[key] = val;
    }
  }

  for (let i = 0; i < args.length; i++) {
    forEach(args[i], assignValue);
  }
  return result;
}

/**
 * 功能等同于合并，区别在于有没有保留原始对象
 *
 * @see merge
 * @param  {...any} args 需要合并的对象
 * @returns {Object} 所有合并属性的结果
 */
function deepMerge(...args) {
  const result = {};
  function assignValue(val, key) {
    if (isObject(result[key]) && isObject(val)) {
      result[key] = deepMerge(result[key], val);
    } else if (isObject(val)) {
      result[key] = deepMerge({}, val);
    } else {
      result[key] = val;
    }
  }

  for (let i = 0; i < args.length; i++) {
    forEach(args[i], assignValue);
  }
  return result;
}

/**
 * 通过拷贝copy对象的属性来扩展extended对象的属性
 *
 * @param {Object} extended 要扩展的对象
 * @param {Object} copy 要复制属性的对象
 * @param {Object} thisArg 要绑定功能的对象
 * @returns {Object} 扩展后的对象
 */
function extend(extended, copy, thisArg) {
  forEach(copy, function assignValue(val, key) {
    if (thisArg && isFunction(val)) {
      extended[key] = bind(val, thisArg);
    } else {
      extended[key] = val;
    }
  });
  return extended;
}

module.exports = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  deepMerge: deepMerge,
  extend: extend,
  trim: trim,
};
